# coding=utf-8
import os
import json
import time
import xml.etree.ElementTree as ET
import shutil
import psycopg2
import sys
import subprocess

LOG_FILE = '/opt/epic/espc/log/upgrade.log'
STATUS_FILE = '/opt/epic/espc/tmp/upgrade.progress'
NODE_INFO = '/opt/epic/espc/majordomo/conf/nodes.json'
CONF_INFO = '/opt/epic/espc/majordomo/conf/conf.json'
BACKUP_NODE_INFO = 'node.info'
BACKUP_PATH = '/opt/rollback'
DST_PATH = '/opt/epic'
CUR_PATH = os.path.join(os.getcwd(), os.path.dirname(__file__))
START_TIME = int(time.time())

reload(sys)
sys.setdefaultencoding('utf8')

CUR_VERSION = ""
PATCH_VERSION = ""


# 记录日志
def record_log(level, msg):
    print('[' + level + ']   ' + msg)
    log_time = time.strftime('[%Y-%m-%d-%H-%M-%S]', time.localtime(time.time()))
    logfile = os.path.normpath(LOG_FILE)
    fp = open(logfile, 'a+')
    if not fp:
        return
    try:
        msg = '[' + level + ']   ' + log_time + '   ' + msg
        fp.write(msg + "\n")
    except Exception, e:
        print(str(e))
    finally:
        fp.close()
    pass


# 获取数据库配置信息
def get_confinfo():
    conf_file = os.path.normpath(CONF_INFO)
    if not os.path.exists(conf_file):
        record_log('ERROR', 'the conf info file does not exist! {0}'.format(conf_file))
        return None
    fp = open(conf_file, 'r')
    if not fp:
        record_log('ERROR', 'the conf info file open error! {0}'.format(conf_file))
        return None
    conf_info = None
    try:
        conf_info = json.loads(fp.read())
    except Exception, e:
        record_log('ERROR', str(e))
    finally:
        fp.close()

    return conf_info


# 获取当前节点信息
def get_nodeinfo():
    node_file = os.path.normpath(NODE_INFO)
    if not os.path.exists(node_file):
        record_log('ERROR', 'the node info file does not exist! {0}'.format(node_file))
        return None

    fp = open(node_file, 'r')
    if not fp:
        record_log('ERROR', 'the node info file open error! {0}'.format(node_file))
        return None
    node_info = None
    try:
        node_info = json.loads(fp.read())
    except Exception, e:
        record_log('ERROR', str(e))
    finally:
        fp.close()

    return node_info


# 处理当前节点信息
def set_nodeinfo(node_info):
    # 备份nodeinfo文件至backup目录
    node_file = os.path.normpath(NODE_INFO)
    backup_file = os.path.normpath(BACKUP_PATH + os.sep + BACKUP_NODE_INFO)
    if not os.path.exists(BACKUP_PATH):
        os.makedirs(BACKUP_PATH)
    copy_file(node_file, backup_file)
    # 更新当前nodeinfo文件
    fp = open(node_file, 'w')
    if not fp:
        record_log('ERROR', 'open the file error! {0}'.format(node_file))
        return
    try:
        fp.write(json.dumps(node_info, indent=4, encoding='UTF-8', ensure_ascii=False))
    except Exception, e:
        record_log('ERROR', str(e))
    finally:
        fp.close()


# 执行SQL脚本
def exec_sql(sql_file):
    conf_info = get_confinfo()
    if conf_info is None:
        return False
    dbaddr = conf_info['postgreSQL']['host']
    dbport = conf_info['postgreSQL']['port']
    dbuser = conf_info['postgreSQL']['user']
    dbpswd = conf_info['postgreSQL']['pwd']
    dbname = conf_info['postgreSQL']['dbname']
    conn = psycopg2.connect(database=dbname, user=dbuser, password=dbpswd, host=dbaddr, port=dbport)
    cur = conn.cursor()
    fp = open(sql_file)
    try:
        sql_text = fp.read()
        if not sql_text:
            record_log('ERROR', 'exe_sql error! the sql_text is null')
            return False
        cur.execute(sql_text)
        conn.commit()
        cur.close()
        conn.close()
    except Exception, e:
        record_log('ERROR', str(e))
    finally:
        fp.close()

def import_sql_file(sql_file):
    os.putenv("PGPASSWORD","85e11609511899a4928ec8900667baaf");
    os.system('/opt/epic/soc/deps/env/bin/psql -U socpostgres -d soc < ' + sql_file);
# 检查版本
def check_version(cur_version, dst_version):
    # 解析当前版本号
    cur_list = cur_version.split('.')
    dst_list = dst_version.split('.')
    cur_major = int(cur_list[0]) * 10000
    cur_minor = int(cur_list[1]) * 1000
    cur_build = int(cur_list[2]) * 100
    cur_revision = int(cur_list[3])
    cur_sp = 0
    if len(cur_list) > 4:
        cur_sp = int(cur_list[4])
    # 解析升级包版本号
    dst_major = int(dst_list[0]) * 10000
    dst_minor = int(dst_list[1]) * 1000
    dst_build = int(dst_list[2]) * 100
    dst_revision = int(dst_list[3])
    dst_sp = 0
    if len(dst_list) > 4:
        dst_sp = int(dst_list[4])
    # 计算版本号
    cur_ver = cur_major + cur_minor + cur_build + cur_revision
    dst_ver = dst_major + dst_minor + dst_build + dst_revision
    # 判断当前版本与升级包版本
    if cur_ver < dst_ver:
        return True
    elif cur_ver == dst_ver:
        return cur_sp < dst_sp

    return False


# 检查依赖
def check_dependent(cur_version, dep_version):
    # 解析当前版本号
    cur_list = cur_version.split('.')
    dep_list = dep_version.split('.')
    cur_major = int(cur_list[0]) * 10000
    cur_minor = int(cur_list[1]) * 1000
    cur_build = int(cur_list[2]) * 100
    cur_revision = int(cur_list[3])
    cur_sp = 0
    if len(cur_list) > 4:
        cur_sp = int(cur_list[4])
    # 解析最小依赖版本号
    dep_major = int(dep_list[0]) * 10000
    dep_minor = int(dep_list[1]) * 1000
    dep_build = int(dep_list[2]) * 100
    dep_revision = int(dep_list[3])
    dep_sp = 0
    if len(dep_list) > 4:
        dep_sp = int(dep_list[4])
    # 计算版本号
    cur_ver = cur_major + cur_minor + cur_build + cur_revision
    dep_ver = dep_major + dep_minor + dep_build + dep_revision
    # 判断当前版本与最小依赖版本
    if cur_ver < dep_ver:
        return False
    elif cur_ver == dep_ver:
        if cur_sp < dep_sp:
            return False

    return True


# 备份文件
def copy_file(src_file, dst_file):
    if not os.path.exists(src_file):
        record_log('ERROR', 'the src file does not exist! {0}'.format(src_file))
        return False

    try:
        shutil.copy2(src_file, dst_file)
    except Exception, e:
        record_log('ERROR', 'copy [%s] to [%s]' % (src_file, dst_file))
        return False
    else:
        return True


# 备份目录
def copy_path(src_path, dst_path, backup_path):
    # 遍历src_path下所有文件，包括子目录
    try:
        files = os.listdir(src_path)
        for fi in files:
            src_file = os.path.join(src_path, fi)
            dst_file = os.path.join(dst_path, fi)
            backup_file = os.path.join(backup_path, fi)
            if os.path.isfile(src_file):
                # 如果原环境中存在该文件，则将文件移动至备份目录
                if os.path.exists(dst_file):
                    shutil.move(dst_file, backup_file)
                # 再将升级包中文件拷贝至ESPC环境中
                copy_file(src_file, dst_file)
                record_log('INFO', 'replace:{0}'.format(dst_file))
            # 如果存在子目录
            else:
                # 如果备份目录不存在则创建该目录
                if not os.path.exists(backup_file):
                    os.makedirs(backup_file)
                if not os.path.exists(dst_file):
                    os.makedirs(dst_file)
                # 递归遍历子目录
                copy_path(src_file, dst_file, backup_file)
    except Exception, e:
        record_log('ERROR', str(e))
        handle_progress('100', 'failure')
        return False
    return True


# 备份升级包信息
def backup_patchinfo():
    # 备份install.py文件至backup目录
    src_install_file = os.path.normpath(CUR_PATH + os.sep + 'install.py')
    dst_install_file = os.path.normpath(BACKUP_PATH + os.sep + 'install.py')
    copy_file(src_install_file, dst_install_file)
    # 备份uninstall.py文件至backup目录
    src_install_file = os.path.normpath(CUR_PATH + os.sep + 'uninstall.py')
    dst_install_file = os.path.normpath(BACKUP_PATH + os.sep + 'uninstall.py')
    copy_file(src_install_file, dst_install_file)
    # 备份version.xml文件至backup目录
    src_install_file = os.path.normpath(CUR_PATH + os.sep + 'version.xml')
    dst_install_file = os.path.normpath(BACKUP_PATH + os.sep + 'version.xml')
    copy_file(src_install_file, dst_install_file)


# 处理step节点
def handle_step(node_type, step):
    step_id = step.attrib['id']
    step_module = step.attrib['module']
    # 每个阶段增加3秒延迟，防止前端获取进度由于执行过快而造成进度条不显示
    time.sleep(3)
    record_log('INFO', 'handle_step: step_id={0}, step_module={1}'.format(step_id, step_module))
    item_list = step.getiterator('item')
    for i in range(len(item_list)):
        try:
            if not handle_item(node_type, item_list[i]):
                handle_progress('100', 'failure')
                return False
        except Exception, e:
            handle_progress('100', 'failure')
            record_log('ERROR', str(e))
            return False
    return True


# 处理item节点
def handle_item(node_type, item):
    item_name = item.attrib['name']
    item_type = item.attrib['type']
    item_progress = item.attrib['progress']
    src_file = os.path.normpath(CUR_PATH + os.sep + item_name)
    dst_file = os.path.normpath(DST_PATH + os.sep + item_name)
    dst_path = os.path.dirname(dst_file)
    backup_file = os.path.normpath(BACKUP_PATH + os.sep + item_name)
    backup_path = os.path.dirname(backup_file)
    record_log('INFO', 'handle_item: item_name={0}, item_type={1}, item_progress={2}'.format(item_name, item_type,
                                                                                             item_progress))
    # 执行脚本
    if (item_type == 'execute') or (item_type == 'upgrade:execute') or (item_type == 'master:execute'):
        # 如果升级包内该文件不存在，则返回失败
        if not os.path.exists(src_file):
            handle_progress('100', 'failure')
            record_log('ERROR', 'the patch file is not exist! file:{0}'.format(src_file))
            return False

        if item_name[-3:] == '.py':
            cmd = 'python {0} >>{1} 2>&1'.format(src_file, LOG_FILE)
        elif item_name[-3:] == '.sh':
            cmd = '{0} >>{1} 2>&1'.format(src_file, LOG_FILE)
        else:
            cmd = src_file

        # 如果是升级时执行，则需判断其返回值,0表示成功，其他表示失败
        if item_type == 'upgrade:execute':
            pipe = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
            output = pipe.communicate()[0]
            re = pipe.returncode
            if re != 0:
                handle_progress('100', 'failure')
                record_log('ERROR', '{0} result:{1}'.format(cmd, re))
                record_log('ERROR', '{0}'.format(output))
                return False
            record_log('INFO', '{0} result:{1}'.format(cmd, re))
        elif item_type == 'master:execute':
            if node_type == 'master':
                re = os.popen(cmd).read().lstrip().rstrip()
                record_log('INFO', '{0} result:{1}'.format(cmd, re))
        else:
            re = os.popen(cmd).read().lstrip().rstrip()
            record_log('INFO', '{0} result:{1}'.format(cmd, re))
        # 如果备份目录不存在则创建该目录
        if not os.path.exists(backup_path):
            os.makedirs(backup_path)
        # 将升级包中脚本迁移至backup目录
        copy_file(src_file, backup_file)
    # 新增文件
    elif item_type == 'add':
        # 如果升级包内该文件不存在，则返回失败
        if not os.path.exists(src_file):
            handle_progress('100', 'failure')
            record_log('ERROR', 'the patch file is not exist! file:{0}'.format(src_file))
            return False
        # 如果目标文件存在，则提示用户修改节点类型为replace
        if os.path.exists(dst_file):
            os.remove(dst_file)
            record_log('WARN',
                       'the dst file is exist! please modify the item type to [replace]! file: {0}'.format(item_name))
        # 如果目标目录不存在则创建该目录
        if not os.path.exists(dst_path):
            os.makedirs(dst_path)
        # 将新增文件移动至ESPC环境中
        copy_file(src_file, dst_file)
        record_log('INFO', 'add:{0}'.format(dst_file))
    # 删除文件
    elif item_type == 'delete':
        # 如果备份目录不存在则创建该目录
        if not os.path.exists(backup_path):
            os.makedirs(backup_path)
        # 如果为模糊删除,则遍历整个目录，删除所有相关文件
        if item_name.endswith('*'):
            file_name = os.path.basename(item_name)
            ilen = len(file_name)
            # record_log('INFO', 'delete:{0}'.format(dst_path + os.sep + file_name))
            for each_file in os.listdir(dst_path):
                # 如果each_file是一个文件，且文件名以file_name开始，则移动该文件至备份目录
                if os.path.isfile(dst_path + os.sep + each_file) and each_file.startswith(file_name[:ilen - 1]) == True:
                    shutil.move(dst_path + os.sep + each_file, backup_path + os.sep + each_file)
                    record_log('INFO', 'delete:{0}'.format(dst_path + os.sep + each_file))
        # 如果原环境中存在该文件，则将文件移动至备份目录
        elif os.path.exists(dst_file):
            if os.path.isfile(dst_file):
                shutil.move(dst_file, backup_file)
            elif os.path.isdir(dst_file):
                record_log('WARN',
                           'the item is directory! please modify the item type to [delete:path]! file: {0}'.format(
                               item_name))
                shutil.move(dst_file, backup_path)
            record_log('INFO', 'delete:{0}'.format(dst_file))
    # 删除目录
    elif item_type == 'delete:path':
        # 如果备份目录不存在则创建该目录
        if not os.path.exists(backup_path):
            os.makedirs(backup_path)
        # 如果原环境中存在该文件，则将文件移动至备份目录
        if os.path.exists(dst_file):
            if os.path.isfile(dst_file):
                shutil.move(dst_file, backup_file)
                record_log('WARN',
                           'the item is file! please modify the item type to [delete]! file: {0}'.format(item_name))
            elif os.path.isdir(dst_file):
                shutil.move(dst_file, backup_path)
            record_log('INFO', 'delete_path:{0}'.format(dst_file))
    # 替换文件
    elif item_type == 'replace' or item_type == 'replace:master':
        # 如果升级包内该文件不存在，则返回失败
        if (item_type == 'replace:master' and node_type == 'master') or item_type == 'replace':
            if not os.path.exists(src_file):
                handle_progress('100', 'failure')
                record_log('ERROR', 'the patch file is not exist! file:{0}'.format(src_file))
                return False
            # 如果替换单个文件
            if os.path.isfile(src_file):
                # 如果备份目录不存在则创建该目录
                if not os.path.exists(backup_path):
                    os.makedirs(backup_path)
                if not os.path.exists(dst_path):
                    os.makedirs(dst_path)
                # 如果原环境中存在该文件，则将文件移动至备份目录
                if os.path.exists(dst_file):
                    shutil.move(dst_file, backup_file)
                # 再将升级包中文件拷贝至ESPC环境中
                copy_file(src_file, dst_file)
                record_log('INFO', 'replace:{0}'.format(dst_file))
            # 替换整个升级包目录下已有文件
            else:
                # 如果备份目录不存在则创建该目录
                if not os.path.exists(backup_file):
                    os.makedirs(backup_file)
                if not os.path.exists(dst_file):
                    os.makedirs(dst_file)
                copy_path(src_file, dst_file, backup_file)
    # 替换目录
    elif item_type == 'replace:path':
        # 如果升级包内该文件不存在，则返回失败
        if not os.path.exists(src_file):
            handle_progress('100', 'failure')
            record_log('ERROR', 'the patch path is not exist! file:{0}'.format(src_file))
            return False
        record_log('INFO', 'the backup path:{0}'.format(backup_path))
        record_log('INFO', 'the dst path:{0}'.format(dst_path))
        # 如果备份目录不存在则创建该目录
        if not os.path.exists(backup_path):
            os.makedirs(backup_path)
        # 如果原环境中存在该文件，则将文件移动至备份目录
        record_log('INFO', 'the backup file:{0}'.format(backup_file))
        record_log('INFO', 'the dst file:{0}'.format(dst_file))
        if os.path.exists(dst_file):
            shutil.move(dst_file, backup_path)
        if not os.path.exists(dst_path):
            os.makedirs(dst_path)
        # 再将升级包中文件夹拷贝至ESPC环境中
        shutil.copytree(src_file, dst_file)
    elif item_type == 'upgrade:sql':
        # 如果升级包内该文件不存在，则返回失败
        if not os.path.exists(src_file):
            handle_progress('100', 'failure')
            record_log('ERROR', 'the patch file is not exist! file:{0}'.format(src_file))
            return False
        # 此处调用数据库安装脚本,只有主节点执行,保证只调用一次
        if node_type == 'master':
            exec_sql(src_file)
    elif item_type == 'import:sql':
        # 如果升级包内该文件不存在，则返回失败
        if not os.path.exists(src_file):
            handle_progress('100', 'failure')
            record_log('ERROR', 'the patch file is not exist! file:{0}'.format(src_file))
            return False
        # 此处调用数据库安装脚本,只有主节点执行,保证只调用一次
        if node_type == 'master':
            import_sql_file(src_file)
    else:
        # 如果升级包内该文件不存在，则返回失败
        if not os.path.exists(src_file):
            handle_progress('100', 'failure')
            record_log('ERROR', 'the patch file is not exist! file:{0}'.format(src_file))
            return False
        # 如果备份目录不存在则创建该目录
        if not os.path.exists(backup_path):
            os.makedirs(backup_path)
        # 将升级包中该文件拷贝至备份目录
        copy_file(src_file, backup_file)
        record_log('INFO', 'Skip the item! name={0},type={1},progress={2}'.format(item_name, item_type, item_progress))
    if item_progress == '100':
        handle_progress(item_progress, 'succeed')
    else:
        handle_progress(item_progress, 'upgrading')
    return True


# 处理progress
def handle_progress(progress, status):
    status_file = os.path.normpath(STATUS_FILE)
    status_path = os.path.dirname(status_file)
    # 如果目标目录不存在则创建该目录
    if not os.path.exists(status_path):
        os.makedirs(status_path)
    fp = open(status_file, 'w')
    if not fp:
        record_log('ERROR', 'open the status file error! {0}'.format(status_file))
        return
    try:
        end_time = 0
        if (status == 'succeed') or (status == 'failure'):
            end_time = int(time.time())
        upgrade_status = {}
        upgrade_status['type'] = 'upgrade'
        upgrade_status['status'] = status
        upgrade_status['progress'] = int(progress)
        upgrade_status['startTime'] = START_TIME
        upgrade_status['endTime'] = end_time
        fp.write(json.dumps(upgrade_status, indent=4, encoding='UTF-8', ensure_ascii=False))
    except Exception, e:
        record_log('ERROR', str(e))
    finally:
        fp.close()


def main():
    # 检查操作系统
    # import platform
    # version = platform.linux_distribution()
    # if version[0] == 'Red Hat Enterprise Linux Server' or version[0] == 'CentOS':
    #     if version[1].split('.')[0] != '6':
    #         record_log('ERROR', 'OS release mismatch!')
    #         handle_progress('100', 'failure')
    #         print '4'
    #         return 'checkfailed'
    # else:
    #     record_log('ERROR', 'OS version mismatch!')
    #     handle_progress('100', 'failure')
    #     print '4'
    #     return 'checkfailed'
    # 读取XML文件
    try:
        xmlfile = os.path.normpath(CUR_PATH + os.sep + 'version.xml')
        record_log('INFO', 'xmlfile:{0}'.format(xmlfile))
        root = ET.parse(xmlfile).getroot()
    except Exception, e:
        record_log('ERROR', str(e))
        print '1'
        return 'failed'
    # 获取节点信息
    patch_showid = root.find('fileinfo').find('showId').text
    patch_codeid = root.find('fileinfo').find('codeId').text
    patch_date = root.find('fileinfo').find('date').text
    node_info = get_nodeinfo()
    if node_info is None:
        print '2'
        return 'nodeinfofailed'
    cur_showid = node_info['showId']
    lastindex = cur_showid.rfind('.')

    # 当前系统为定制系统
    if cur_showid[5:6] == 'M' or lastindex > 5:
        # 当前升级包非定制升级包，返回失败
        if patch_showid[5:6] != 'M' and patch_showid < 5:
            record_log('INFO', 'can not upgrade current custom espc; cur_showid:{0}, patch_showid:{}'.format(cur_showid,
                                                                                                             patch_showid))
            print '3'
            return 'failed'
    cur_version = node_info['version']
    cur_codeid = node_info['code_version']
    node_type = node_info['type']

    # 检查升级包版本
    patch_version = root.attrib['version']
    if cur_version != patch_version:
        if not check_version(cur_version, patch_version):
            record_log('ERROR', 'check_version error! {0}:{1}'.format(cur_version, patch_version))
            print '4'
            return 'checkfailed'
    else:
        if int(cur_codeid) >= int(patch_codeid):
            record_log('ERROR', 'check_version error! {0} codeId {1}:{2}'.format(cur_version, cur_codeid, patch_codeid))
            print '5'
            return 'checkfailed'

    # 初始化当前版本和升级包版本
    global CUR_VERSION, PATCH_VERSION
    CUR_VERSION = cur_version
    PATCH_VERSION = patch_version
    record_log('INFO', 'current version is {0}, patch version is {1}'.format(cur_version, patch_version))

    # 检查升级包最小依赖
    dep_version = root.find('fileinfo').find('depId').text
    if not check_dependent(cur_version, dep_version):
        record_log('ERROR', 'check_dependent error! {0}:{1}'.format(cur_version, dep_version))
        print '6'
        return 'checkfailed'
    record_log('INFO', 'the patch version:{0},start upgrading'.format(patch_version))

    # 如果rollback目录存在，则删除该目录
    if os.path.exists(BACKUP_PATH):
        shutil.rmtree(BACKUP_PATH)

    # 开始升级
    step_list = root.getiterator('step')
    for i in range(len(step_list)):
        # 处理version.xml中的step节点
        # node_type为节点类型，可以为master or slave
        if not handle_step(node_type, step_list[i]):
            print '7'
            return 'installfailed'

    # 备份install.py、uninstall.py和version.xml
    backup_patchinfo()

    # 修改nodeinfo文件，完成升级
    new_node_info = get_nodeinfo()
    new_node_info['version'] = patch_version
    new_node_info['showId'] = patch_showid
    new_node_info['shipTime'] = patch_date[0:4] + '-' + patch_date[4:6] + '-' + patch_date[6:8]
    if patch_codeid is not None:
        new_node_info['code_version'] = patch_codeid
    set_nodeinfo(new_node_info)
    record_log('INFO', 'upgrade successful! the new version is {0}'.format(patch_version))
    print '0'
    return 'succeed'


if __name__ == '__main__':
    if len(sys.argv) > 1:
        CUR_PATH = sys.argv[1]
    main()
